/**
 * Copyright (c) Huawei Technologies Co., Ltd. 2019-2021. All rights reserved.
 */

#include <gtest/gtest.h>

#include "runtime/mem/gc/gc.h"
#include "runtime/include/runtime.h"
#include "runtime/mem/heap_manager.h"
#include "runtime/mem/gc/g1/g1-allocator.h"
#include "runtime/include/thread_scopes.h"
#include "plugins/ecmascript/runtime/js_thread.h"
#include "plugins/ecmascript/runtime/ecma_string.h"
#include "plugins/ecmascript/runtime/tagged_array.h"
#include "plugins/ecmascript/runtime/js_hclass-inl.h"
#include "plugins/ecmascript/runtime/global_env_constants-inl.h"

namespace ark::ecmascript {
class G1GCBarrierTest : public testing::Test {
public:
    NO_COPY_SEMANTIC(G1GCBarrierTest);
    NO_MOVE_SEMANTIC(G1GCBarrierTest);

    // NOLINTNEXTLINE(readability-magic-numbers)
    explicit G1GCBarrierTest(size_t promotionRegionAliveRate = 100)
    {
        RuntimeOptions options;
        options.SetLoadRuntimes({"ecmascript"});
        options.SetGcType("g1-gc");
        options.SetRunGcInPlace(true);
        options.SetCompilerEnableJit(false);
        options.SetGcWorkersCount(0);
        options.SetG1PromotionRegionAliveRate(promotionRegionAliveRate);
        options.SetGcTriggerType("debug-never");
        options.SetShouldLoadBootPandaFiles(false);
        options.SetShouldInitializeIntrinsics(false);
        options.SetExplicitConcurrentGcEnabled(false);

        Runtime::Create(options);

        thread = JSThread::GetCurrent();
        const GlobalEnvConstants *globalConst = thread->GlobalConstants();
        stringClass_ = JSHClass::Cast(globalConst->GetStringClass().GetTaggedObject());
        arrayClass_ = JSHClass::Cast(globalConst->GetArrayClass().GetTaggedObject());
    }

    ~G1GCBarrierTest() override
    {
        Runtime::Destroy();
    }

    ObjectHeader *AllocObject()
    {
        EcmaVM *vm = EcmaVM::Cast(thread->GetVM());
        return EcmaString::CreateEmptyString(vm);
    }

    TaggedArray *AllocArray(size_t length)
    {
        size_t size = TaggedArray::ComputeSize(JSTaggedValue::TaggedTypeSize(), length);
        mem::HeapManager *heapManager = thread->GetVM()->GetHeapManager();
        auto *array = reinterpret_cast<TaggedArray *>(
            heapManager->AllocateObject(arrayClass_->GetHClass(), size, TAGGED_OBJECT_ALIGNMENT, thread));
        array->SetLength(length);
        return array;
    }

    // NOLINTNEXTLINE(misc-non-private-member-variables-in-classes)
    JSThread *thread;

private:
    JSHClass *arrayClass_;
    JSHClass *stringClass_;
};

class ConcurrentMarkListener : public mem::GCListener {
public:
    NO_COPY_SEMANTIC(ConcurrentMarkListener);
    NO_MOVE_SEMANTIC(ConcurrentMarkListener);
    ~ConcurrentMarkListener() override = default;

    ConcurrentMarkListener(G1GCBarrierTest *curTest, JSHandle<TaggedArray> arr, JSHandle<ObjectHeader> obj,
                           JSHandle<ObjectHeader> curReplacement)
        : test_(curTest), array_(arr), object_(obj), replacement_(curReplacement)
    {
    }

    void GCPhaseStarted(mem::GCPhase phase) override
    {
        if (phase != mem::GCPhase::GC_PHASE_MARK) {
            return;
        }
        hasConcurrentMark = true;
        JSThread *thread = test_->thread;
        array_->Set(thread, 0, JSTaggedValue::Undefined());
        array_->Set(thread, 1, replacement_.GetTaggedValue());

        PandaVector<ObjectHeader *> *preBuff = thread->GetPreBuff();
        EXPECT_TRUE(preBuff != nullptr);
        if (preBuff == nullptr) {
            return;
        }
        EXPECT_EQ(1U, preBuff->size());
        EXPECT_EQ(object_.GetObject<ObjectHeader>(), preBuff->front());
    }

    // NOLINTNEXTLINE(misc-non-private-member-variables-in-classes)
    bool hasConcurrentMark = false;

private:
    G1GCBarrierTest *test_;
    JSHandle<TaggedArray> array_;
    JSHandle<ObjectHeader> object_;
    JSHandle<ObjectHeader> replacement_;
};

TEST_F(G1GCBarrierTest, TestPreBarrier)
{
    ScopedManagedCodeThread s(thread);
    [[maybe_unused]] EcmaHandleScope scope(thread);
    mem::GC *gc = thread->GetVM()->GetGC();
    JSHandle<TaggedArray> array(thread, AllocArray(2));
    JSHandle<ObjectHeader> obj(thread, AllocObject());
    JSHandle<ObjectHeader> replacement(thread, AllocObject());
    array->Set(thread, 0, obj.GetTaggedValue());        // test object -> undefined
    array->Set(thread, 1, JSTaggedValue::Undefined());  // test undefined -> object

    ConcurrentMarkListener listener(this, array, obj, replacement);
    gc->AddListener(&listener);

    {
        ScopedNativeCodeThread sn(thread);
        GCTask task(GCTaskCause::HEAP_USAGE_THRESHOLD_CAUSE);  // trigger concurrent marking
        task.Run(*gc);
    }
    ASSERT_TRUE(listener.hasConcurrentMark);
}

class G1GCClassCollectionTest : public testing::TestWithParam<bool> {
public:
    NO_COPY_SEMANTIC(G1GCClassCollectionTest);
    NO_MOVE_SEMANTIC(G1GCClassCollectionTest);

    G1GCClassCollectionTest()
    {
        RuntimeOptions options;
        options.SetLoadRuntimes({"ecmascript"});
        options.SetGcType("g1-gc");
        options.SetRunGcInPlace(true);
        options.SetG1TrackFreedObjects(GetParam() ? "true" : "false");
        options.SetCompilerEnableJit(false);
        options.SetGcWorkersCount(0);
        options.SetGcTriggerType("debug-never");
        options.SetShouldLoadBootPandaFiles(false);
        options.SetShouldInitializeIntrinsics(false);

        Runtime::Create(options);

        thread_ = JSThread::GetCurrent();
    }

    ~G1GCClassCollectionTest() override
    {
        Runtime::Destroy();
    }

    JSHandle<JSHClass> AllocClass()
    {
        const GlobalEnvConstants *globalConst = thread_->GlobalConstants();
        ObjectFactory *factory = thread_->GetEcmaVM()->GetFactory();
        JSHClass *rootHclass = JSHClass::Cast(globalConst->GetHClassClass().GetTaggedObject());
        return factory->NewEcmaDynClass(rootHclass, 0, JSType::JS_OBJECT);
    }

    mem::ObjectAllocatorG1<> *GetAllocator()
    {
        Runtime *runtime = Runtime::GetCurrent();
        mem::GC *gc = runtime->GetPandaVM()->GetGC();
        return static_cast<mem::ObjectAllocatorG1<> *>(gc->GetObjectAllocator());
    }

protected:
    // NOLINTNEXTLINE(misc-non-private-member-variables-in-classes)
    JSThread *thread_ {};
};

TEST_P(G1GCClassCollectionTest, TestCollectClasses)
{
    ScopedManagedCodeThread s(thread_);
    JSHClass *hclass = nullptr;
    {
        [[maybe_unused]] EcmaHandleScope scope(thread_);
        JSHandle<JSHClass> hclassHandle = AllocClass();
        // JSHClass is allocated in the non-movable space so we can use raw pointer safe
        hclass = hclassHandle.GetObject<JSHClass>();
    }
    ASSERT_NE(nullptr, hclass);
    mem::Region *region = mem::ObjectToRegion(hclass);
    ASSERT_TRUE(region->HasFlag(mem::IS_NONMOVABLE));
    mem::GC *gc = thread_->GetVM()->GetGC();

    {
        ScopedNativeCodeThread sn(thread_);
        GCTask task(GCTaskCause::HEAP_USAGE_THRESHOLD_CAUSE);  // trigger concurrent marking
        task.Run(*gc);
    }
    bool found = false;
    GetAllocator()->IterateRegularSizeObjects([hclass, &found](ObjectHeader *obj) {
        if (obj == hclass) {
            found = true;
        }
    });
    if (GetParam()) {
        // Tracking freed objects is enabled. We cannot delete the class.
        ASSERT_TRUE(found);
    } else {
        // Tracking freed objects is disabled. We should delete the class.
        ASSERT_FALSE(found);
    }
}

INSTANTIATE_TEST_SUITE_P(G1GCClassCollectionTestSuite, G1GCClassCollectionTest, testing::Values(true, false));
}  // namespace ark::ecmascript
